/**
 * 
 */
package truerefactor.refactor;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;

import truerefactor.graph.CodeGraph;
import truerefactor.graph.CodeNode;
import truerefactor.graph.Edge;
import truerefactor.graph.EdgeType;
import truerefactor.graph.MethodNode;
import truerefactor.graph.StatementNode;
import truerefactor.graph.StatementType;
import truerefactor.graph.TypePair;

/**
 * An Implementation of the Extract Method Refactoring technique
 * 
 * @author Isaac
 */
public class ExtractMethod extends Refactoring {

    /**
     * 
     */
    public ExtractMethod()
    {
        this.shortName = "EXMETH";
        this.name = "Extract Method";
        this.description = "";
    }

    /*
     * (non-Javadoc)
     * @see
     * truerefactor.refactor.Refactoring#execute(truerefactor.graph.CodeNode [])
     */
    @Override
    public void execute(CodeGraph graph)
    {
        int count = 1;
        MethodNode extractFrom = (MethodNode) operands.get(0);
        Set<StatementNode> params = new HashSet<StatementNode>();

        // 1. Create a new method, and name it after the intention of the method
        // (name it by what id does, not how it does it)
        // a. IF the code you want to extract is very simple, such as a single
        // message or function call, you should extract it if the name of the
        // new
        // method will reveal the intention of the code in a better way. If you
        // can't come up with a more meaningful name, don't extract the code.
        MethodNode extracted = new MethodNode(extractFrom.getIdentifier() + "Extract" + count, "");

        List<StatementNode> toExtract = findExtractableCode(extractFrom);

        addVariableDeclarators(extractFrom, toExtract, params);

        fillInMethodBody(extractFrom, extracted, toExtract);

        generateParametersForLocalVars(extractFrom, extracted, toExtract, params);

        checkTemporaryVariables(graph, extracted, toExtract);

        generateMethodCalls(extractFrom, extracted, params);
    }

    /**
     * 
     * @param extractFrom
     * @param extracted
     * @param toExtract
     */
    private void fillInMethodBody(MethodNode extractFrom, MethodNode extracted, List<StatementNode> toExtract)
    {
        // fills in extracted with toExtract using the information in
        // extractFrom's statement graph
        List<StatementNode> toExtractClones = new ArrayList<StatementNode>();
        for (StatementNode node : toExtract)
        {
            try
            {
                toExtractClones.add((StatementNode) node.clone());
            }
            catch (CloneNotSupportedException e)
            {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
        }

        for (StatementNode clone : toExtractClones)
        {
            extracted.getStatementGraph().insert(clone);
            extracted.getStatements().add(clone);
        }

        for (StatementNode clone : toExtractClones)
        {
            List<CodeNode> adjNodes = extractFrom.getStatementGraph().getAdjacencies(clone);

            for (CodeNode node : adjNodes)
            {
                if (extracted.getStatementGraph().get(node) != null)
                {
                    extracted.getStatementGraph().insert(
                            new Edge(clone, extracted.getStatementGraph().get(node), EdgeType.Statement));
                }
            }
        }
    }

    /**
     * 
     * @param extractFrom
     * @param extracted
     * @param params
     */
    private void generateMethodCalls(MethodNode extractFrom, MethodNode extracted, Set<StatementNode> params)
    {
        // 7. Replace the extracted code in the source method with a call to the
        // target method.
        // a. If you have moved any temporary variables over to the target
        // method, look to see whether they were declared outside of the
        // extracted code. If so, you can now remove the declaration.

        // TODO figure out how to install the generated statement
        StringBuilder code = new StringBuilder();
        code.append(extracted.getIdentifier() + "(");
        for (String s : extracted.getParameters())
        {
            code.append(s);
            code.append(", ");
        }
        code.append(");");

        StatementNode insert = new StatementNode(extractFrom, StatementType.EXPRESSION, code.toString());
    }

    /**
     * 
     * @param extractFrom
     * @param extracted
     * @param toExtract
     * @param params
     */
    private void generateParametersForLocalVars(MethodNode extractFrom, MethodNode extracted,
            List<StatementNode> toExtract, Set<StatementNode> params)
    {
        // 6. Pass into the target method as parameters local-scope variables
        // that are read from the extracted code.

        HashMap<String, TypePair> paramInfo = MethodNode.extractParamInfo(params);

        for (String var : paramInfo.keySet())
        {
            extractFrom.addParameter(var, paramInfo.get(var));
        }
    }

    /**
     * 
     * @param graph
     * @param extracted
     * @param toExtract
     * @return
     */
    private boolean checkTemporaryVariables(CodeGraph graph, MethodNode extracted, List<StatementNode> toExtract)
    {
        // 5. Look to see whether any of these local-scope variables are
        // modified by the extracted code. If one variable is modified, see
        // whether you can treat the extracted code as a query and assign the
        // results to the variable concerned. If this is awkward, or if there is
        // more than one such variable, you can't extract the method as it
        // stands. You may need to use Split Temporary Variable and try again.
        // You can eliminate temporary variables with Replace Temp with Query.

        // FIXME Currently we just assume that it can't be done, but later we
        // will fix this

        int numTemps = 0;
        boolean retVal = false;
        HashMap<String, StatementNode> temps = extracted.findTempVars();

        for (String tempVar : temps.keySet())
        {
            for (StatementNode node : toExtract)
            {
                if (node.equals(temps.get(tempVar)))
                    continue;

                String code = node.getCode();
                if (code.contains(tempVar.trim()))
                {
                    if (code.contains("=") && code.indexOf(tempVar.trim()) < code.indexOf("="))
                        numTemps++;
                }
            }
        }

        if (numTemps > 1)
        {
            retVal = false;
        }
        else
        {
            CodeNode operands[] = { extracted };
            Refactoring ref = RefactoringFactory.getInstance().createReplaceTempWithQuery(operands);
            ref.execute(graph);
            retVal = true;
        }

        return retVal;
    }

    /**
     * 
     * @param extractFrom
     * @param toExtract
     * @param params
     */
    private void addVariableDeclarators(MethodNode extractFrom, List<StatementNode> toExtract, Set<StatementNode> params)
    {
        // 4. See whether any temporary variables are used only within this
        // extracted code. If so, declare them in the target method as temporary
        // variables.
        HashMap<String, StatementNode> temps = extractFrom.findTempVars();

        List<StatementNode> otherNodes = new ArrayList<StatementNode>();
        List<StatementNode> tempList = new ArrayList<StatementNode>();
        for (String key : temps.keySet())
        {
            tempList.add(temps.get(key));
        }

        for (CodeNode node : extractFrom.getStatements())
        {
            if (node instanceof StatementNode)
            {
                otherNodes.add((StatementNode) node);
            }
        }

        otherNodes.removeAll(tempList);
        HashMap<StatementNode, List<StatementNode>> tempDecMap = new HashMap<StatementNode, List<StatementNode>>();

        List<StatementNode> toRemove = new ArrayList<StatementNode>();
        for (StatementNode node : otherNodes)
        {
            for (String s : temps.keySet())
            {
                if (!node.getCode().contains(s))
                {
                    if (tempDecMap.keySet().contains(temps.get(s)))
                    {
                        List<StatementNode> temp = tempDecMap.get(temps.get(s));
                        temp.add(node);
                    }
                    else
                    {
                        List<StatementNode> temp = new ArrayList<StatementNode>();
                        temp.add(node);
                        tempDecMap.put(temps.get(s), temp);
                    }
                }
                else
                {
                    toRemove.add(node);
                }
            }
        }

        Set<StatementNode> toAdd = new HashSet<StatementNode>();
        for (StatementNode node : tempDecMap.keySet())
        {
            List<StatementNode> uses = new ArrayList<StatementNode>();
            for (StatementNode use : tempDecMap.get(node))
            {
                if (toExtract.contains(use))
                {
                    uses.add(use);
                }
            }

            List<StatementNode> temp = tempDecMap.get(node);
            temp.removeAll(uses);

            if (temp.isEmpty())
            {
                toAdd.add(node);
            }
            else
            {
                params.add(node);
            }
        }

        for (StatementNode node : toAdd)
        {
            toExtract.add(0, node);
        }
    }

    /**
     * 
     * @param extractFrom
     * @return
     */
    private List<StatementNode> findExtractableCode(MethodNode extractFrom)
    {
        List<StatementNode> retVal = new ArrayList<StatementNode>();
        // 2. Copy the extracted code from the source method into the new target
        // method.

        // FIXME: Last section need to determine how to read the graph to find
        // portions of extractable code.
        CodeGraph sourceGraph = extractFrom.getStatementGraph();
        List<CodeNode> sources = sourceGraph.getSources();
        
        
        // FIXME: for now just randomly select a group of nodes
        int numStmts = extractFrom.getStatements().size();
        int pos = (int) (Math.random() * Math.ceil(numStmts));
        
        if (pos < numStmts - 1) {
            int depth = (int) (Math.random() * ((numStmts - 1) - pos));
            
            for (int i = pos; i < pos + depth; i++) {
                CodeNode node = extractFrom.getStatements().get(i);
                if (node instanceof StatementNode) {
                    retVal.add((StatementNode) node);
                }
            }
        }
        
        return retVal;
    }
}
